package com.xiaoyu.im.provider;

import android.text.TextUtils;

import androidx.annotation.Nullable;

import com.xiaoyu.base.dao.ConversationDao;
import com.xiaoyu.im.data.ConversationData;
import com.xiaoyu.base.DBCall;
import com.xiaoyu.base.data.UserInfoDataProvider;
import com.xiaoyu.base.entity.ConversationEntity;
import com.xiaoyu.base.entity.MessageEntity;
import com.xiaoyu.base.data.UserData;
import com.xiaoyu.im.ChatToken;
import com.xiaoyu.im.datamodel.ConversationType;
import com.xiaoyu.im.datamodel.LastContentWrapper;
import com.xiaoyu.im.datamodel.common.IMConstant;
import com.xiaoyu.im.IMLog;
import com.xiaoyu.im.client.IMClient;
import com.xiaoyu.im.datamodel.AppMessage;
import com.xiaoyu.im.datamodel.vendors.VendorConversationUnique;
import com.xiaoyu.base.utils.ListUtil;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import in.srain.cube.util.CheckUtil;
import in.srain.cube.request.JsonData;
import in.srain.cube.util.CLog;
import in.srain.cube.util.internal.AppFunction;
import in.srain.cube.util.internal.AppSupplier;
import in.srain.cube.concurrent.AppThreads;

public class ConversationManager {

    private static final String TAG = "ConversationManager";
    private static final ConversationManager INSTANCE = new ConversationManager();
    private static AppFunction<AppMessage, LastContentWrapper> sLastContentFunction;

    private final AtomicReference<ChatToken> mCurrentChatToken = new AtomicReference<>();
    private final Map<String, ConversationEntity> mConversationMap = Collections.synchronizedMap(new LinkedHashMap<>());

    private final AtomicBoolean mViewVisible = new AtomicBoolean();
    private AppSupplier<Boolean> mVisibleSupplier;
    private ConversationListener mConversationListener;

    public static ConversationManager getInstance() {
        return INSTANCE;
    }

    private ConversationManager() {
    }

    public void setVisibleSupplier(AppSupplier<Boolean> visibleSupplier) {
        this.mVisibleSupplier = visibleSupplier;
    }

    public void setConversationListener(ConversationListener conversationListener) {
        this.mConversationListener = conversationListener;
    }

    public static void init(AppFunction<AppMessage, LastContentWrapper> lastContentFunction) {
        sLastContentFunction = lastContentFunction;
    }

    public static LastContentWrapper getLastContentWrapper(AppMessage message) {
        return sLastContentFunction != null ? sLastContentFunction.invoke(message) : new LastContentWrapper();
    }

    public void updateFromJsonList(final List<JsonData> list) {
        List<ConversationEntity> entities = ListUtil.convertToList(list, ConversationDao::updateEntity);
        DBCall.listCall(database -> database.conversationDao().upsertAll(entities));
        List<String> chatIds = ListUtil.convertToList(entities, ConversationEntity::getChatId);
        reloadLastMessage(chatIds); // temp
        updateConversationList();
    }

    private void clearChatHistory(String chatId) {
        DBCall.run(database -> database.messageDao().deleteMessageByChatId(chatId));
        if (mConversationListener != null) {
            mConversationListener.clearChatHistory(chatId);
        }
    }

    private void clearAllChatHistory() {
        DBCall.run(database -> database.messageDao().deleteAllMessage());
        // TODO @sanshi IMClient.getInstance().sendMessageReceipt(AppMessage.fromEntity(messageEntity));
        if (mConversationListener != null) {
            mConversationListener.clearAllChatHistory();
        }
    }

    private void deleteConversation(String chatId) {
        updateConversationAsyncAfterDelete(chatId, () -> DBCall.run(database -> database.conversationDao().delete(chatId)));
        clearChatHistory(chatId);
    }

    private void deleteAllConversation() {
        updateAllConversationAsyncAfterDelete(() -> DBCall.run(database -> database.conversationDao().deleteAll()));
        clearAllChatHistory();
    }

    public void deleteConversationAsync(String chatId) {
        AppThreads.runOnIOThread(() -> deleteConversation(chatId));
    }

    public void deleteAllConversationAsync() {
        AppThreads.runOnIOThread(this::deleteAllConversation);
    }

    private void updateLastMessageAsync(String chatId) {
        updateConversationAsync(chatId, () -> DBCall.run(database -> database.conversationDao().updateLastMessage(chatId)));
    }

    private void updateUnreadCountAsync(String chatId, int count) {
        updateConversationAsync(chatId, () -> DBCall.run(database -> database.conversationDao().updateUnreadCount(chatId, count)));
    }

    public void updateConversationMuteAsync(String chatId, boolean mute) {
        updateConversationAsync(chatId, () -> DBCall.run(database -> database.conversationDao().updateStatus(chatId, ConversationEntity.STATUS_MUTE, (mute ? ConversationEntity.STATUS_MUTE : 0))));
    }

    private void updateConversationAsync(String chatId, Runnable runnable) {
        AppThreads.runOnIOThread(() -> {
            runnable.run();
            loadConversation(chatId);
        });
    }

    private void updateAllConversationAsyncAfterDelete(Runnable runnable) {
        AppThreads.runOnIOThread(() -> {
            runnable.run();
            updateConversationList();
            ChatIdMapper.getInstance().clear();
        });
    }

    private void updateConversationAsyncAfterDelete(String chatId, Runnable runnable) {
        AppThreads.runOnIOThread(() -> {
            runnable.run();
            loadConversationAfterDelete(chatId);
        });
    }

    private void updateTopWeight(String chatId, long topWeight) {
        DBCall.run(database -> database.conversationDao().updateTopWeight(chatId, topWeight));
    }

    public void updateTopWeightAsync(String chatId, long topWeight) {
        updateConversationAsync(chatId, () -> updateTopWeight(chatId, topWeight));
    }

    public void updateFromJsonListAsync(List<JsonData> list) {
        AppThreads.runOnIOThread(() -> updateFromJsonList(list));
    }

    public void loadConversationAfterDelete(String chatId) {
        mConversationMap.remove(chatId);
        ChatIdMapper.getInstance().delete(chatId);
        notifyItemListUpdate();
    }

    public boolean loadConversation(String chatId) {
        ConversationEntity entity = DBCall.call(database -> database.conversationDao().getConversation(chatId));
        if (entity == null) {
            return false;
        }
        mConversationMap.put(entity.getChatId(), entity);
        ChatIdMapper.getInstance().update(entity);
        notifyItemListUpdate();
        return true;
    }

    public void updateConversationList() {
        CLog.d(IMConstant.LOG_TAG, "updateConversationList");
        loadConversationListFromDB();
        notifyItemListUpdate();
    }

    public void loadConversationListFromDBAsync() {
        AppThreads.runOnDBThread(this::loadConversationListFromDB);
    }

    public void loadConversationListFromDB() {
        List<ConversationEntity> conversations = DBCall.listCall(database -> database.conversationDao().getAllConversations());
        CLog.d(IMConstant.LOG_TAG, "loadConversationListFromDB %s", conversations.size());

        UserInfoDataProvider.getInstance().ensureUser(ListUtil.convertToSetFilterNull(conversations, ConversationEntity::getToUid));

        synchronized (mConversationMap) {
            mConversationMap.clear();

            for (ConversationEntity conversation : conversations) {
                mConversationMap.put(conversation.getChatId(), conversation);
                ChatIdMapper.getInstance().update(conversation);
            }
        }
    }

    public void onConversationOpen(ChatToken chatToken) {
        CLog.d(TAG, "NimProxy conversation open");
        mCurrentChatToken.set(chatToken);
        readConversation(chatToken.getChatId());
        trySendLatestMessageReceipt();
        if (mConversationListener != null) {
            mConversationListener.onConversationOpen(chatToken);
        }
    }

    public void onConversationClose(ChatToken chatToken) {
        CLog.d(TAG, "NimProxy conversation close");
        if (mConversationListener != null) {
            mConversationListener.onConversationClose(chatToken);
        }
        mCurrentChatToken.compareAndSet(chatToken, null);
    }

    public void updateViewVisible(boolean visible) {
        if (!mViewVisible.compareAndSet(!visible, visible)) {
            return;
        }

        final ChatToken chatToken = mCurrentChatToken.get();
        CLog.d(TAG, "NimProxy conversation update visibility");
        if (mViewVisible.get() && chatToken != null && !TextUtils.isEmpty(chatToken.getChatId())) {
            readConversation(chatToken.getChatId());
            trySendLatestMessageReceipt();
        }
        CLog.d(TAG, "updateViewVisible visible: %s chatToken: %s", mViewVisible.get(), chatToken);
    }

    public ChatToken getCurrentChatToken() {
        return mCurrentChatToken.get();
    }

    public boolean isCurrentOpenConversation(String chatId) {
        final ChatToken chatToken = mCurrentChatToken.get();
        return chatToken != null && TextUtils.equals(chatToken.getChatId(), chatId);
    }

    public boolean isCurrentOpenConversation(ChatToken chatToken) {
        if (chatToken == null || (TextUtils.isEmpty(chatToken.getChatId()) && TextUtils.isEmpty(chatToken.getUid()))) {
            return false;
        }
        final ChatToken currentChatToken = mCurrentChatToken.get();
        return currentChatToken != null && currentChatToken.isMatch(chatToken);
    }

    public void ensureConversation(String chatId) {
        CheckUtil.checkNotMain();
        ensureConversation(Collections.singleton(chatId));
    }

    public void ensureConversation(Collection<String> chatIds) {
        CheckUtil.checkNotMain();
        CLog.d(TAG, "ensureConversation: %s", chatIds);
        List<String> fetchChatIds = new ArrayList<>();
        for (String chatId : chatIds) {
            if (!TextUtils.isEmpty(chatId) && !isConversationInLocal(chatId)) {
                fetchChatIds.add(chatId);
            }
        }
        if (!fetchChatIds.isEmpty()) {
            boolean success = ConversationData.syncConversationListFromServer(fetchChatIds);
            if (!success) {
                String desc = String.format("ensure conversation, chatIds=%s", TextUtils.join(",", chatIds));
                IMLog.logNetError("fetch_conv_by_chat_id", desc);
            }
        }
    }

    public boolean ensureConversationByVendor(VendorConversationUnique unique) {
        CheckUtil.checkNotMain();
        return ensureConversationByVendor(Collections.singleton(unique));
    }

    public List<AppMessage> ensureConversationMapAndGetMessagesByVendor(Map<VendorConversationUnique, List<AppMessage>> conversationMessageListMap) {
        CheckUtil.checkNotMain();
        List<VendorConversationUnique> uniques = new LinkedList<>(conversationMessageListMap.keySet());
        ensureConversationByVendor(uniques);

        List<AppMessage> messages = new LinkedList<>();
        for (Map.Entry<VendorConversationUnique, List<AppMessage>> entry : conversationMessageListMap.entrySet()) {
            messages.addAll(ensureConversationAndReceiveMessages(entry.getKey(), entry.getValue()));
        }
        return messages;
    }

    private List<AppMessage> ensureConversationAndReceiveMessages(VendorConversationUnique unique, List<AppMessage> messages) {
        String uid = UserData.getInstance().getUid();
        boolean success = ensureConversationByVendor(unique);
        if (!success) {
            String desc = String.format("unique=%s", unique);
            IMLog.logNetError("fetch_conv_by_vendor_id", desc);
            return Collections.emptyList();
        }

        String chatId = ChatIdMapper.getInstance().getChatIdByVendor(unique);
        if (TextUtils.isEmpty(chatId)) {
            String desc = String.format("unique=%s, chatId is empty", unique);
            IMLog.logNetError("fetch_conv_by_vendor_id", desc);
            return Collections.emptyList();
        }
        List<MessageEntity> entities = new ArrayList<>();
        for (int i = 0; i < messages.size(); i++) {
            MessageEntity entity = messages.get(i).toEntity();
            entity.setChatId(chatId);
            entity.setAttributes(JsonData.newMap().toString());
            entities.add(entity);

            AppMessage message = AppMessage.fromEntity(entity);
            messages.set(i, message);
            IMLog.logImStreamForEnsureMessage(message);
        }
        DBCall.run(uid, database -> database.messageDao().upsertAll(entities));
        return messages;
    }

    private boolean ensureConversationByVendor(Collection<VendorConversationUnique> uniques) {
        CheckUtil.checkNotMain();
        CLog.d(TAG, "ensureConversationByVendor: %s", uniques);
        List<VendorConversationUnique> fetchUniques = new ArrayList<>();
        for (VendorConversationUnique unique : uniques) {
            final String chatId = ChatIdMapper.getInstance().getChatIdByVendor(unique);
            if (!isConversationInLocal(chatId)) {
                fetchUniques.add(unique);
            }
        }
        if (!fetchUniques.isEmpty()) {
            return ConversationData.syncConversationListFromServerByVendorUniques(fetchUniques);
        }
        return true;
    }

    public void ensureConversationByUid(String uid, String fromType) {
        CheckUtil.checkNotMain();
        CLog.d(TAG, "ensureConversationByUid: %s", uid);
        final String chatId = ChatIdMapper.getInstance().getChatIdByUid(uid);
        if (!TextUtils.isEmpty(uid) && !isConversationInLocal(chatId)) {
            ConversationData.createConversationSync(uid, fromType);
        }
    }

    public boolean isConversationInLocal(@Nullable String chatId) {
        CheckUtil.checkNotMain();
        if (TextUtils.isEmpty(chatId)) {
            return false;
        }
        if (mConversationMap.containsKey(chatId)) {
            return true;
        }
        return loadConversation(chatId);
    }

    public ConversationEntity fetchConversation(String chatId) {
        return chatId == null ? null : mConversationMap.get(chatId);
    }

    public void readConversation(String chatId) {
        CLog.d(TAG, "readConversation() chatId=%s", chatId);
        updateUnreadCountAsync(chatId, 0);
        IMClient.getInstance().clearUnreadCount(chatId);
    }

    private void processMessageVisibleOnMessageIn(String chatId) {
        CLog.d(TAG, "messageIn(), chatId=%s, visible=%s", chatId, mViewVisible.get());
        if (isCurrentOpenConversation(chatId) && mViewVisible.get()) {
            readConversation(chatId);
            trySendLatestMessageReceipt();
        } else {
            int unreadCount = IMClient.getInstance().getUnreadMessageCount(chatId);
            updateUnreadCountAsync(chatId, unreadCount);
        }
    }

    private void trySendLatestMessageReceipt() {
        CLog.d(TAG, "NimProxy trySendLatestMessageReceipt()");
        final ChatToken chatToken = mCurrentChatToken.get();
        if (chatToken == null || !chatToken.hasChatId() || isInvisible()) {
            CLog.d(TAG, "trySendLatestMessageReceipt() chatToken=%s, visibility=%s, is_incognito=%s", chatToken, mViewVisible.get(), isInvisible());
            return;
        }
        final String chatId = chatToken.getChatId();
        sendLatestMessageReceipt(chatId);
    }

    public void sendLatestMessageReceipt(String chatId) {
        String uid = UserData.getInstance().getUid();
        final DBCall.Runnable runnable = database -> {
            final MessageEntity messageEntity = database.messageDao().getNewestReceiveMessage(chatId, uid);
            if (messageEntity != null) {
                CLog.d(TAG, "sendLatestMessageReceipt: %s %s %s %s", chatId, messageEntity.getId(), messageEntity.getTime(), messageEntity.getText());
                IMClient.getInstance().sendMessageReceipt(AppMessage.fromEntity(messageEntity));
            }
        };
        DBCall.runAsync(uid, runnable);
    }

    private boolean isInvisible() {
        return !mViewVisible.get() || (mVisibleSupplier == null ? false : mVisibleSupplier.get());
    }

    public void onMessageIn(String chatId) {
        onMessageIO(chatId);
        processMessageVisibleOnMessageIn(chatId);
    }

    public void onMessageOut(String chatId) {
        onMessageIO(chatId);
    }

    private void onMessageIO(String chatId) {
        CheckUtil.checkNotMain();
        recoverDelete(chatId);
        updateLastMessageAsync(chatId);
    }

    public void recoverDeleteAsync(String chatId) {
        updateConversationAsync(chatId, () -> DBCall.run(database -> database.conversationDao().updateStatus(chatId, ConversationEntity.STATUS_DELETE, 0)));
    }

    public void blackAsync(String chatId, boolean black) {
        updateConversationAsync(chatId, () -> DBCall.run(database -> database.conversationDao().updateStatus(chatId, ConversationEntity.STATUS_BLACK, black ? ConversationEntity.STATUS_BLACK : 0)));
    }

    private void recoverDelete(String chatId) {
        DBCall.run(database -> database.conversationDao().updateStatus(chatId, ConversationEntity.STATUS_DELETE, 0));
    }

    public void onMessageRemove(AppMessage message) {
        ConversationEntity model = fetchConversation(message.chatToken.getChatId());
        if (model != null && TextUtils.equals(model.getLastMessageId(), message.id)) {
            updateLastMessageAsync(message.chatToken.getChatId());
        }
    }

    public void clear() {
        mConversationMap.clear();
        mCurrentChatToken.set(null);
    }

    private ConversationEntity[] mapToList() {
        Collection<ConversationEntity> values = mConversationMap.values();
        return values.toArray(new ConversationEntity[0]);
    }

    public boolean isSingleChat(ConversationEntity conversation) {
        return conversation == null || TextUtils.equals(conversation.getType(), ConversationType.SINGLE);
    }

    private void reloadLastMessage(List<String> chatIds) { // temp
        DBCall.run(database -> {
            for (String chatId : chatIds) {
                MessageEntity messageEntity = database.messageDao().getNewestLastContentMessage(chatId);
                if (messageEntity != null) {
                    database.conversationDao().updateLastMessage(chatId, messageEntity.getId(), messageEntity.getDisplayTime());
                }
            }
        });
    }

    public List<ConversationEntity> buildDisplayConversationList() {
        final List<ConversationEntity> list = new LinkedList<>();
        for (ConversationEntity conversation : mapToList()) {
            if (conversation.isDelete()) {
                continue;
            }
            if (isSingleChat(conversation) && TextUtils.isEmpty(conversation.getToUid())) {
                CLog.w(IMConstant.LOG_TAG, "skip cause nobody %s", conversation);
                continue;
            }
            list.add(conversation);
        }
        return list;
    }

    private void notifyItemListUpdate() {
        if (mConversationListener != null) {
            mConversationListener.notifyItemListUpdate();
        }
    }

    public boolean isConversationEmpty() {
        synchronized (mConversationMap) {
            return mConversationMap.isEmpty();
        }
    }
}
